#include <windows.h>
#include <d3d9.h>
#include <d3dx9tex.h>
#include <Dxerr.h>  
#include <stdio.h>
#include <dinput.h>

#include "grid.h"


#define SCREEN_WIDTH  1024
#define SCREEN_HEIGHT 768

#define NUM_DEVICES 2

// macros for keyboard and mouse button presses
#define  KEYDOWN(name, key) (name[key] & 0x80)
#define  BUTTONDOWN(name, key) (name.rgbButtons[key] & 0x80)

DIMOUSESTATE			mouseState;				// holds the current state of the mouse device


HINSTANCE hInst;				// holds the instance for this app
HWND wndHandle;					// global window handle

LPDIRECT3D9             pD3D;
LPDIRECT3DDEVICE9       pd3dDevice;

// camera variables
D3DXMATRIX matView;					// the view matrix
D3DXMATRIX matProj;					// the projection matrix
D3DXVECTOR3 camPos;					// the position of the camera
D3DXVECTOR3 camLook;				// where the camera is pointing
D3DXVECTOR3 camDir;					// camera direction vector

LPDIRECTINPUT8			DIObject;
LPDIRECTINPUTDEVICE8	pDIDevice[NUM_DEVICES];
int NumDevices = 0;

float cameraAngleHoriz = 0.0f, cameraAngleVert = 0.0f;
float const cameraMoveSpeed = 1.0f;
bool Wireframe = false;
int NumFrames = 0;
LARGE_INTEGER TimerStart, TimerEnd, TimerFreq;

Grid                    *pGrid;


LPDIRECT3DTEXTURE9 texture = NULL;

////////////////////////////////////////////// forward declarations
bool    initWindow(HINSTANCE hInstance);
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);

bool initDirect3D(HWND hwnd);
void shutdownDirect3D(void);

void createCamera(float nearClip, float farClip);
void moveCamera(D3DXVECTOR3 vec);
void pointCamera(D3DXVECTOR3 vec);

// The callback function to enumerate the installed devices
BOOL CALLBACK    EnumDevicesCallback( const DIDEVICEINSTANCE* pDI_instance, VOID* pContext );

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow)
{
	HRESULT hr;
	LPD3DXFONT pFont;
	char fpsStr[128] = "\0";
	RECT fpsRect = {0, 0, 300, 20};
	char keyboardBuffer[256];

	// call our function to init and create our window
	if (!initWindow(hInstance))
	{
		MessageBox(NULL, "Unable to create window", "ERROR", MB_OK);
		return false;
	}

	if (!initDirect3D(wndHandle))
	{
		MessageBox(NULL, "Unable to init Direct3D", "ERROR", MB_OK);
		return false;
	}

	
	// create the direct input object
	DirectInput8Create(hInstance, DIRECTINPUT_VERSION, IID_IDirectInput8, 
					   (void**)&DIObject, NULL);

	// enumerate all the connected devices
	DIObject->EnumDevices(DI8DEVCLASS_ALL, EnumDevicesCallback, NULL, DIEDFL_ALLDEVICES); 

	pDIDevice[0]->SetDataFormat(&c_dfDIKeyboard);
	
	pDIDevice[1]->SetDataFormat(&c_dfDIMouse);
	
	for (int i = 0; i < NUM_DEVICES; ++i)
	{
		// Set the cooperative level 
		pDIDevice[i]->SetCooperativeLevel(wndHandle, DISCL_FOREGROUND | DISCL_NONEXCLUSIVE); 

		// Get access to the input device. 
		pDIDevice[i]->Acquire(); 
	}

	// Create a D3DX font object
	D3DXCreateFont( pd3dDevice, 18, 0, FW_BOLD, 0, FALSE, 
		DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, DEFAULT_QUALITY, DEFAULT_PITCH | FF_DONTCARE, "Arial", 
		&pFont );

	camPos = D3DXVECTOR3(0.0f, 40.0f, -20.0f);
	camLook = D3DXVECTOR3(0.0f, 40.0f, 0.0f);
	camDir = D3DXVECTOR3(0.0f, 0.0f, 1.0f);
	cameraAngleHoriz = atan2f(camLook.z - camPos.z, camLook.x - camPos.x);
	
	D3DXVECTOR3 v0(-500.0f, 0.0f, -500.0f);
	D3DXVECTOR3 v1( 500.0f, 0.0f, -500.0f);
	D3DXVECTOR3 v2( 500.0f, 0.0f,  500.0f);
	D3DXVECTOR3 v3(-500.0f, 0.0f,  500.0f);

	pGrid = new Grid(101, 101, &v0, &v1, &v2, &v3);
	pGrid->CreateDirectXMesh(pd3dDevice);

	D3DXCreateTextureFromFile(pd3dDevice, "grass.jpg", &texture);

	createCamera(0.1f, 1000.0f);		// near clip plane, far clip plane
			
	// init the timer frequency and timer
	QueryPerformanceFrequency(&TimerFreq);
	QueryPerformanceCounter(&TimerStart);

	// Main message loop:
	// Enter the message loop
	MSG msg; 
	ZeroMemory( &msg, sizeof(msg) );
	while( msg.message!=WM_QUIT )
	{
		// check for messages
		if( PeekMessage( &msg, NULL, 0U, 0U, PM_REMOVE ) )
		{
			TranslateMessage( &msg );
			DispatchMessage( &msg );
		}
		// this is called when no messages are pending
		else
		{
			// setup mouse handling
			pDIDevice[1]->GetDeviceState(sizeof(DIMOUSESTATE), &mouseState);
	
			RECT r;
			static int mouseX = SCREEN_WIDTH / 2, mouseY = SCREEN_HEIGHT / 2;
			GetWindowRect(wndHandle, &r);
			SetCursorPos(r.left + mouseX, r.top + mouseY);

			//-----------------< horizontal look axis >----------------------------------
			cameraAngleHoriz -= 0.01f * mouseState.lX;

			// wrap the angle
			if (cameraAngleHoriz > (PI * 2))
			{
				cameraAngleHoriz -= (PI * 2);
			}

			if (cameraAngleHoriz < 0)
			{
				cameraAngleHoriz += (PI * 2);
			}
			//---------------------------------------------------------------------------

			//-----------------< vertical look axis >------------------------------------
			cameraAngleVert -= 0.01f * mouseState.lY;

			// wrap the angle
			if (cameraAngleVert > (PI * 2))
			{
				cameraAngleVert -= (PI * 2);
			}
			
			if (cameraAngleVert < 0)
			{
				cameraAngleVert += (PI * 2);
			}

			camDir = D3DXVECTOR3( cosf(cameraAngleHoriz) * 20.0f, sinf(cameraAngleVert) * 20.0f, 
								  (sinf(cameraAngleHoriz) + cosf(cameraAngleVert)) * 20.0f );

			camLook = camPos + camDir;
			//---------------------------------------------------------------------------

			// check keyboard input
			pDIDevice[0]->GetDeviceState(sizeof(keyboardBuffer), (void*) keyboardBuffer);

			if (KEYDOWN(keyboardBuffer, DIK_UP))
			{
				camPos += camDir * 0.02f;
			}
			if (KEYDOWN(keyboardBuffer, DIK_DOWN))
			{
				camPos -= camDir * 0.02f;
			}
			if (KEYDOWN(keyboardBuffer, DIK_RIGHT))
			{
				D3DXVECTOR3 temp;
				D3DXVec3Cross(&temp, &D3DXVECTOR3(0.0f, 1.0f, 0.0f), &camDir);
				camPos += temp * 0.01f;
			}
			if (KEYDOWN(keyboardBuffer, DIK_LEFT))
			{
				D3DXVECTOR3 temp;
				D3DXVec3Cross(&temp, &D3DXVECTOR3(0.0f, 1.0f, 0.0f), &camDir);
				camPos -= temp*0.01f;
			}
			
			pGrid->GetGridPoint(&camPos, camPos.x, camPos.z);
			camPos.y += 20.0f;
			
			moveCamera(camPos);
			pointCamera(camLook);


			// Clear the backbuffer to a black color
			pd3dDevice->Clear( 0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, 
							  D3DCOLOR_XRGB(5, 5, 5), 1.0f, 0 );

			pd3dDevice->BeginScene();
			//
				// compute the frame rate
				if (NumFrames % 100 == 0)
				{
					float fps;
					QueryPerformanceCounter(&TimerEnd);
					fps = NumFrames * TimerFreq.QuadPart / (float)(TimerEnd.QuadPart - TimerStart.QuadPart);

					sprintf(fpsStr, "Frame Rate: %.2f fps", fps);
				}

				// display the frame rate
				pFont->DrawTextA(NULL, fpsStr, (int)strlen(fpsStr), &fpsRect, 0, D3DCOLOR_ARGB(255, 0, 255, 0));

				pd3dDevice->SetTexture(0, texture);

				pGrid->DrawDirectXMesh(pd3dDevice);
			//
			pd3dDevice->EndScene();

			// update the frame counter
			NumFrames++;

			// Present the backbuffer contents to the display
			pd3dDevice->Present( NULL, NULL, NULL, NULL );
		}
	}

	// release and shutdown Direct3D
	shutdownDirect3D();

	return (int) msg.wParam;
}

bool initWindow(HINSTANCE hInstance)
{
	WNDCLASSEX wcex;

	wcex.cbSize = sizeof(WNDCLASSEX);
	wcex.style			= CS_HREDRAW | CS_VREDRAW;
	wcex.lpfnWndProc	= (WNDPROC)WndProc;
	wcex.cbClsExtra		= 0;
	wcex.cbWndExtra		= 0;
	wcex.hInstance		= hInstance;
	wcex.hIcon			= 0;
	wcex.hCursor		= LoadCursor(NULL, IDC_ARROW);
	wcex.hbrBackground	= (HBRUSH)(COLOR_WINDOW+1);
	wcex.lpszMenuName	= NULL;
	wcex.lpszClassName	= "DirectX Lab 5 - Wk 5 Ronald Ram";
	wcex.hIconSm		= 0;
	RegisterClassEx(&wcex);

	wndHandle = CreateWindow("DirectX Lab 5 - Wk 5 Ronald Ram", 
							 "DirectX Lab 5 - Wk 5 Ronald Ram", 
							 WS_OVERLAPPEDWINDOW,
							 CW_USEDEFAULT, 
							 CW_USEDEFAULT, 
							 SCREEN_WIDTH, 
							 SCREEN_HEIGHT, 
							 NULL, 
							 NULL, 
							 hInstance, 
							 NULL);
   if (!wndHandle)
	  return false;
   
   ShowWindow(wndHandle, SW_SHOW);
   UpdateWindow(wndHandle);

   return true;
}

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	switch (message) 
	{
		case WM_DESTROY:
			PostQuitMessage(0);
			break;
	case WM_KEYDOWN:
		int fwKeys;
		LPARAM keyData;
		fwKeys = (int)wParam;    // virtual-key code 
		keyData = lParam;          // key data 

		switch(fwKeys)
		{
		case VK_ESCAPE:
			PostQuitMessage(0);
			break;
		default:
			break;
		}

		break;

	default:
		break;
	}
	return DefWindowProc(hWnd, message, wParam, lParam);
}

bool initDirect3D(HWND hwnd)
{
	if( NULL == ( pD3D = Direct3DCreate9( D3D_SDK_VERSION ) ) )
		return false;
	
	D3DPRESENT_PARAMETERS d3dpp; 
	ZeroMemory( &d3dpp, sizeof(d3dpp) );
	d3dpp.Windowed = TRUE;
	d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
	d3dpp.BackBufferFormat = D3DFMT_UNKNOWN;
	d3dpp.BackBufferCount  = 1;
	d3dpp.BackBufferHeight = SCREEN_HEIGHT;
	d3dpp.BackBufferWidth  = SCREEN_WIDTH;
	d3dpp.hDeviceWindow    = hwnd;
	d3dpp.EnableAutoDepthStencil = true;
	d3dpp.AutoDepthStencilFormat = D3DFMT_D16;
	
	if( FAILED( pD3D->CreateDevice( D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hwnd,
									  D3DCREATE_SOFTWARE_VERTEXPROCESSING,
									  &d3dpp, &pd3dDevice ) ) )
	return false;

	if (Wireframe) {
		pd3dDevice->SetRenderState(D3DRS_FILLMODE, D3DFILL_WIREFRAME);
	} else {
		pd3dDevice->SetRenderState(D3DRS_FILLMODE, D3DFILL_SOLID);
	}
	pd3dDevice->SetRenderState(D3DRS_LIGHTING, false);

	pd3dDevice->SetRenderState(D3DRS_ZENABLE, true);
	
	return true;
}

void shutdownDirect3D(void)
{
	if( pd3dDevice != NULL) 
	{
		pd3dDevice->Release();
		pd3dDevice = NULL;
	}
	if( pD3D != NULL)
	{
		pD3D->Release();
		pD3D = NULL;
	}
}

/*************************************************************************
* createCamera
* creates a virtual camera
*************************************************************************/
void createCamera(float nearClip, float farClip)
{
	//Here we specify the field of view, aspect ration and near and far clipping planes.
	D3DXMatrixPerspectiveFovLH(&matProj, D3DX_PI/4, float(640)/480, nearClip, farClip);
	pd3dDevice->SetTransform(D3DTS_PROJECTION, &matProj);

}

/*************************************************************************
* moveCamera
* moves the camera to a position specified by the vector passed as a 
* parameter
*************************************************************************/
void moveCamera(D3DXVECTOR3 vec)
{
	camPos = vec;
}

/*************************************************************************
* pointCamera
* points the camera a location specified by the passed vector
*************************************************************************/
void pointCamera(D3DXVECTOR3 vec)
{
	camLook = vec;

	D3DXMatrixLookAtLH(&matView, &camPos,		//Camera Position
								 &camLook,		//Look At Position
								 &D3DXVECTOR3(0.0f, 1.0f, 0.0f));		//Up Direction

	pd3dDevice->SetTransform(D3DTS_VIEW, &matView);
}

/*************************************************************************
* provides input device callback
*************************************************************************/
BOOL CALLBACK EnumDevicesCallback( const DIDEVICEINSTANCE* pDI_instance,
									 VOID* pContext )
{
	HRESULT hr;

	// create the DirectInput device
	hr = DIObject->CreateDevice(GUID_SysKeyboard, &pDIDevice[0], NULL);
	if( FAILED(hr) ) 
		return DIENUM_CONTINUE;
	hr = DIObject->CreateDevice(GUID_SysMouse, &pDIDevice[1], NULL);
	if( FAILED(hr) ) 
		return DIENUM_STOP;

	return DIENUM_CONTINUE;
}